package com.zytech.zspider.core;

import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zytech.modules.lang.Ghost;
import com.zytech.modules.lang.Requires;
import com.zytech.modules.lang.Strings;
import com.zytech.modules.lang.annotations.Immutable;

import java.io.Serializable;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @author siuming
 */
@Immutable
public final class TaskResult implements Serializable {

    public static final TaskResult EMPTY = null;
    private static final long serialVersionUID = -2222429425843342999L;

    private final String taskname;
    private final Map<String, Object> values;
    private final long startTime;
    private final long elapsedTime;

    /**
     * @param builder
     */
    TaskResult(Builder builder) {
        this.taskname = builder.taskname;
        this.values = builder.values;
        this.startTime = builder.startTime;
        this.elapsedTime = builder.elapsedTime;
    }

    /**
     * @return
     */
    public String getTaskname() {
        return taskname;
    }

    /**
     * @return
     */
    public Map<String, Object> getValues() {
        return Collections.unmodifiableMap(values);
    }

    /**
     * @return
     */
    public long getStartTime() {
        return startTime;
    }

    /**
     * @return
     */
    public long getElapsedTime() {
        return elapsedTime;
    }

    /**
     * @param another
     * @return
     */
    public TaskResult combine(final TaskResult another) {
        Requires.notNull(another, "another must not be null.");

        Map<String, Object> results = combineResults(getValues(), another.getValues());
        long totalElapsedTime = getElapsedTime() + another.getElapsedTime();
        return TaskResult
                .ofBuilder()
                .taskname(getTaskname())
                .values(results)
                .startTime(getStartTime())
                .elapsedTime(totalElapsedTime)
                .build();
    }

    private Map<String, Object> combineResults(Map<String, Object> results, final Map<String, Object> another) {
        // result'name not start with [a-z] will be discard.
        Map<String, Object> dups = Maps.filterKeys(results, new Predicate<String>() {
            @Override
            public boolean apply(String input) {
                return another.containsKey(input) && reserveResult(input);
            }
        });

        Map<String, Object> merge = Maps.transformEntries(dups, new Maps.EntryTransformer<String, Object, Object>() {
            @Override
            public Object transformEntry(String key, Object value) {
                List<Object> result = Lists.newArrayList();
                mergeResultValue(value, result);
                mergeResultValue(another.get(key), result);
                return result;
            }
        });

        Map<String, Object> accepts = Maps.filterKeys(results, new Predicate<String>() {
            @Override
            public boolean apply(String input) {
                return reserveResult(input);
            }
        });

        Map<String, Object> acceptAnos = Maps.filterKeys(another, new Predicate<String>() {
            @Override
            public boolean apply(String input) {
                return reserveResult(input);
            }
        });

        Map<String, Object> result = Maps.newHashMap();
        result.putAll(accepts);
        result.putAll(acceptAnos);
        result.putAll(merge);
        return result;
    }

    private boolean reserveResult(String input) {
        return !Strings.isBlank(input) && input.matches("^[a-z].*");
    }

    private void mergeResultValue(Object value, List<Object> result) {
        if (null == value) {
            return;
        }

        Ghost<?> ghost = Ghost.me(value);
        if (ghost.openEyes().isArray()) {
            Object[] values = (Object[]) value;
            for (Object val : values) {
                result.add(val);
            }
            return;
        }

        if (ghost.openEyes().isIterable()) {
            Iterable<Object> values = (Iterable<Object>) value;
            for (Object val : values) {
                result.add(val);
            }
            return;
        }

        if (ghost.openEyes().isIterator()) {
            Iterator<Object> it = (Iterator<Object>) value;
            while (it.hasNext()) {
                result.add(it.next());
            }
        }

        result.add(value);
    }

    @Override
    public String toString() {
        return Objects
                .toStringHelper(this)
                .add("taskname", getTaskname())
                .add("values", getValues())
                .add("startTime", getStartTime())
                .add("elapsedTime", getElapsedTime())
                .toString();
    }

    /**
     * @return
     */
    public static Builder ofBuilder() {
        return new Builder();
    }

    public static class Builder {

        String taskname;
        long startTime;
        long elapsedTime;
        Map<String, Object> values = Maps.newHashMap();

        Builder() {
        }

        public Builder taskname(String taskname) {
            Requires.hasText(taskname, "taskname must not be null or empty.");
            this.taskname = taskname;
            return this;
        }

        public Builder values(Map<String, Object> values) {
            Requires.notNull(values, "values must not be null.");
            this.values = values;
            return this;
        }

        public Builder startTime(long startTime) {
            Requires.isTrue(startTime > 0, "startTime must gt 0.");
            this.startTime = startTime;
            return this;
        }

        public Builder elapsedTime(long elapsedTime) {
            Requires.isTrue(elapsedTime > 0, "elapsedTime must gt 0.");
            this.elapsedTime = elapsedTime;
            return this;
        }

        public TaskResult build() {
            return new TaskResult(this);
        }
    }
}
